Charlie Calvert's C++ Builder Unleashed
- 4 -
Events
In this chapter you take a look at the basic facts about the BCB delegation model.
Events are fundamental to the use of this programming environment, and you will never
be able to do serious work in BCB without understanding them.
Subjects covered in this chapter include
- Using the mouse and the keyboard
- Responding directly to Windows messages such as WM_KEYDOWN or WM_MOUSEMOVE
- Custom event structures such as TWMMouse or TMessage
- Menus and menu IDs
- Setting up a WndProc in a VCL application
- Responding to WM_COMMAND messages
This chapter is not designed to be difficult, but rather to hit the highlights
of each of these subjects so you can understand how to use events in your own programs.
By the time you are through with this chapter, you should have a thorough understanding
of the BCB delegation model and will know how to use events in your own program.
Additional material on events is included in the chapters that cover creating components.
Events: The BCB Delegation Model
The two central ideas behind the type of RAD programming of which BCB partakes
are components and delegation. Components are treated at length in Part IV of this
book, called "Creating Components." This is where I will talk about delegation.
Delegation is an alternative to inheritance. It's a trick to allow you to receive
the same benefits as inheritance but with less work.
Delegation is not entirely original with RAD programming; however, it is taken
much further in this paradigm than elsewhere. A part of classic windows programming
that supports delegation is the way you handle standard WM_COMMAND messages.
Think for a moment about standard Windows programming as it appeared before RAD
rewrote the book on Windows programming. I'm talking the Windows programming of Petzold,
or of my Teach Yourself Windows 95 Programming in 21 Days (Sams Publishing) book.
Suppose that in a standard Windows program you have a button that responds to a particular
event, say a message click. You usually handled that click inside the WM_COMMAND
section of the window procedure (WndProc). Windows was delegating the event
from the button to your WM_COMMAND handler. In other words, Windows did
not force you to subclass the button class in order to respond to clicks
on a button.
This is the central idea behind the delegation model in BCB. In C++Builder, if
you drop a button on a form, you can set up an OnClick event handler to
handle clicks on the button. The great advantage this system has over the standard
Windows model is ease of use. BCB will create the message handler for you, asking
that you write only the code that responds to the event. Specifically, it fills in
the class definition with a declaration for your event in the header:
class TForm1 : public TForm
{
__published:
TButton *Button1;
void __fastcall Button1Click(TObject *Sender); // DECLARATION HERE!
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
And it creates the even handler itself in your CPP file:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
}
This is what is meant by the delegation model in BCB. What is radical about the
VCL's use of the delegation model is that it appears everywhere. All sorts of components
support the model and enable you to use it to do all sorts of things that required
subclassing in standard Windows programming and in OWL or MFC.
One of the primary goals of the delegation model is to allow the main form in
your application to handle code that you would have had to use inheritance to handle
in OWL or MFC. In the inheritance model you had to override a constructor to change
the way an object was initialized. In BCB, you just respond to an OnCreate
event. If you want to modify the things a user can type into an edit control, you
don't have to subclass the control; instead, you just respond to OnKeyDown
events.
Delegation is easier than inheritance. It enables neophytes who don't understand
inheritance a way to get up to speed quickly. More importantly, it enables programmers
to get their work done quickly without a lot of repetitive typing.
Of course, if you want to use inheritance, it is available. In fact, inheritance
is used all the time in BCB. The point is not to eliminate a powerful technique like
inheritance, but to give you an alternative to use in the vast majority of cases
where inheritance is overkill.
The Delegation Model and Contract-Free
Programming
The delegation model supports something called contract-free programming. In some
semiliterate circles this has also been known as "contractless" programming.
The idea behind contract-free programming is that there is no contract between
the developer of a component and the user of a component as to what can and can't
be done inside an event handler.
A classic example of a contract-bound program is found in Windows when you handle
WM_KILLFOCUS message. There are some things you can and cannot do in response
to this message. For example, if you change the focus during your response to this
message, you can crash Windows. There is a contract between you and Windows not to
change the focus while responding to this message. Learning all the things you can
and cannot do in response to a message is a long, error-prone, and frustrating process.
BCB supports contract-free programming, which means you can do whatever you want
while responding to an event. BCB is religious in pursuit of this goal. In my code,
I strive to achieve this same goal, though I admit that I have been known to cheat.
When I do so, however, I severely limit the usability of my components.
The Basics of the Delegation Model
Event-oriented code is one of the central tenets of Windows programming. Some
rapid- application development environments attempt to hide users from this feature
altogether, as if it were something so complicated that most programmers couldn't
understand it. The truth is that event-oriented programming is not, in itself, particularly
complex. However, some features of the way it is implemented in Windows can be confusing
under certain circumstances.
BCB gives you full access to the event-oriented substructure that provides Windows
with a high degree of power and flexibility. At the same time, it simplifies and
clarifies the way a programmer handles those events. The end result is a system that
gives you complete access to the power of Windows while simultaneously protecting
you from unnecessary complexity.
These next few sections cover the following topics:
- Event-oriented programming basics
- Responding to mouse events or key events
- Accessing the information passed in events
- The basics of sets, which are used frequently in BCB event handlers
- Circumventing BCB message handling tools and directly capturing messages
- Creating WM_COMMAND handlers and finding the IDs of the components used
in a program
To be really good at programming Windows, you need to be able to look at these
subjects from both the perspective of a RAD programmer and of a Windows API programmer.
That is, you need to know the VCL inside out, and you need to know the Windows API
inside out. This chapter concentrates on the VCL side of that equation, whereas the
other side is featured in books such as Teach Yourself Windows 95 Programming in
21 Days (Sams Publishing) and Programming Windows (Microsoft Press). Put both perspectives
together and you can go on to create your own VCL components or you can design elegant,
well-structured Windows programs.
Before starting, let me reiterate that BCB hides much of the complexity of Windows
programming. However, the developers did not want to prevent programmers from accessing
any portion of the Windows API. By the time you finish reading this section of the
chapter, you should be able to see that BCB gives you access to the full range of
power provided by an event- oriented system.
BCB Events
BCB makes it easy to handle keyboard and mouse events. Suppose, for instance,
you wanted to capture left mouse clicks in the main form of your program. Here's
how to get started. Create a new project and name it EVENTS1.MAK.
In the Object Inspector for the main form, choose the Events Page and double-click
the area to the right of the OnClick property. Create the following function:
void __fastcall TForm1::FormClick(TObject *Sender)
{
MessageDlg("The delegation model says hello.", mtInformation,
TMsgDlgButtons() << mbOK, 0);
}
This code tells Windows that a dialog box should appear every time the user clicks
the left mouse button in the form. The dialog box is shown in Figure 4.1.
FIGURE
4.1. The dialog box displayed by the EVENTS1
program when you click the left mouse button inside the main form.
The previous code presents one of the simplest possible cases of responding to an
event in a BCB program. It is so simple, in fact, that many programmers write this
kind of code without ever understanding that they are writing event-oriented code.
In this case, BCB programmers get the event secondhand, because VCL massages the
event before passing it on to the main form. Nonetheless, this is real event-oriented
programming, albeit in a very simplified manifestation.
As you saw back in the section on the Windows API, the operating environment notifies
you not only of the event, but of several related bits of information. For instance,
when a mouse- down event is generated, a program is informed about where the event
occurred and which button generated the event. If you want to get access to this
kind of relatively detailed information, you should turn to the Events Page for the
form and create an OnMouseDown handler:
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (Shift.Contains(ssRight))
{
Canvas->Brush->Style = bsClear;
Canvas->TextOut(X, Y, "* Button");
}
}
This method writes text to the form every time the user clicks the right mouse
button. It sets the brush to bsClear style to make the background of text
transparent.
To test this method, run the program and click the right mouse button in several
different locations in the form. You'll see that each spot you click is marked, as
shown in Figure 4.2.
FIGURE
4.2. When you click the right mouse button
in the form of the EVENTS1 program, the location of the event is recorded.
The Canvas->TextOut function prints text to the screen at the location
specified by the variables X and Y. Both of these variables are
supplied to you by BCB, which received them in turn from the operating system. The
X variable tells you the column in which the mouse was clicked, and the
Y variable tells you the row.
As you can see, BCB makes it simple for you to respond to events. Furthermore,
not only mouse events are easy to handle. You can respond to keypresses in a similar
manner. For instance, if you create a method for the OnKeyDown property
on the Events Page for the form, you can show the user which key was pressed on the
keyboard whenever the EVENTS1 program has the focus:
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
MessageDlg(Key, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
In the preceding code, the MessageDlg function will pop up the ASCII
value associated with a keystroke. In other words, BCB and the operating system both
pass you not an actual letter like A, B, or C, but the
number associated with the key you pressed. On PCs, the letter A is associated
with the number 65. It wouldn't be appropriate for BCB to perform this translation
for you automatically, because some keys, such as the F1 or Enter key, have no letter
associated with them. Later in this chapter you learn how to use OnKeyDown
events to respond sensibly to keypresses on special keys such as F1, Shift, or Caps
Lock.
Besides OnKeyDown events, BCB also lets you respond to keyboard activity
through the OnKeyPress event:
void __fastcall TForm1::FormKeyPress(TObject *Sender, char &Key)
{
AnsiString S("OnKeyPress: " + AnsiString(Key));
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
You can see that this event is similar to an OnKeyDown event. The difference
is that the Key variable passed to OnKeyPress events is already
translated into a char. However, OnKeyPress events work only for
the alphanumeric keys and are not called when special keys are pressed. In short,
the OnKeyPress event is the same as a WM_CHAR event.
The code for the EVENTS1 program is shown in Listing 4.1. Get the program up and
running and take whatever time is necessary to be sure it all makes sense to you.
There is no point in trying to be a Windows programmer if you don't understand events.
Listing 4.1. The main form for the
EVENTS1 program.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (Shift.Contains(ssRight))
{
Canvas->Brush->Style = bsClear;
Canvas->TextOut(X, Y, "* Button");
}
}
void __fastcall TForm1::DelegateMe(TObject *Sender)
{
MessageDlg("The menu says hello.", mtInformation,
TMsgDlgButtons() << mbOK, 0);
}
void __fastcall TForm1::FormClick(TObject *Sender)
{
MessageDlg("The delegation model says hello.", mtInformation,
TMsgDlgButtons() << mbOK, 0);
}
void __fastcall TForm1::FormKeyPress(TObject *Sender, char &Key)
{
AnsiString S("OnKeyPress: " + AnsiString(Key));
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
MessageDlg(Key, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
After this introduction to event-oriented programming, it's time to step back
and see some of the theory behind the code. After explaining something of how the
system works, this chapter goes on to give examples of how to take full advantage
of Windows event-oriented code base.
Understanding Events
Event-oriented programming isn't unique to Windows, nor is it a chore that can
be handled only by an operating system. For instance, any DOS program could be based
around a simple loop that keeps running the entire time the program is in memory.
Here is a hypothetical example of how such code might look:
do
{
CheckForMouseEvent(Events);
CheckForKeyPress(Events)
HandleEvents(Events);
} while (!Events.Done);
This code represents a typical event-oriented loop. A simple do..while
statement checks for keyboard and mouse events and then calls HandleEvents
to give the program a chance to respond to the events that are generated by the user
or the operating system.
The variable called Events might be a record with a fairly simple structure:
struct TEvent {
int X, Y;
TButton MouseButton;
int Key;
bool Done;
};
X and Y give the current location of the cursor, and Key
contains the value of the top event in the key buffer. The TButton type
might have a declaration that looks like this:
enum TButton {ButtonLeft, ButtonRight};
These structures permit you to track where the mouse is, what state its buttons
are in, and what keys the user has pressed. Admittedly, this is a simple type of
event structure, but the principles involved mirror what is going on inside Windows
or inside other event-oriented systems such as Turbo Vision. If the program being
written was an editor, pseudo-code for the HandleEvent for the program might
look like this:
void HandleEvent(TEvent: Events)
{
switch(Events.Key)
{
case "A..z":
WriteXY(Events.X, Events.Y, Events.Key);
break;
case EnterKey:
Write(CarriageReturn);
break;
case EscapeKey:
Events.Done = TRUE;
break;
}
}
Given the preceding code, the program would go to location X,Y and write
the letter most recently pressed by the user. If the Enter key was pressed, a carriage
return would be written to the screen. A press on the Esc key would cause the program
to terminate. All other keypresses would be ignored.
Code like this can be very powerful, particularly if you're writing a program
that requires animation. For instance, if you need to move a series of bitmaps across
the screen, you want to move the bitmap a few pixels and then check to see whether
the user has pressed a button or hit a keystroke. If an event has occurred, you want
to handle it. If nothing occurred, you want to continue moving the bitmap.
I hope the short code samples shown here give you some feeling for the way event-oriented
systems work. The only piece that's missing is an understanding of why Windows is
event- oriented.
Microsoft made Windows event-oriented in part because multiple programs run under
the environment at the same time. In multitasking systems, the operating system needs
to know whether the user has clicked in a program or whether the click was in the
desktop window. If the mouse click occurred in a window that was partially hidden
behind another window, it is up to the operating system to recognize the event and
bring that window to the foreground. Clearly, it wouldn't be appropriate for the
window itself to have to be in charge of that task. To ask that much would place
an impossible burden on the programmer who created the window. As a result, it's
best for the operating system to handle all the keystrokes and mouse clicks, and
to then pass them on to the various programs in the form of events. Any other system
would force every programmer to handle all the events that occurred when his or her
program had the focus, and to manipulate the entire operating system in response
to certain mouse events or keystrokes, such as Alt+Tab.
In short, Windows programmers almost never directly monitor the hardware or hardware
interrupts. Instead, the operating system handles that task. It passes all external
events on to individual programs in the form of messages. In a typical event-oriented
system, the operating system continually polls the hardware in a loop and then sends
each event off to its programs in the form of some kind of event structure or event
variables. This is the same kind of activity you saw in the brief code snippets shown
earlier.
You have seen that Windows handles mouse and keyboard events, and passes them
on to the appropriate window. The message that is generated in these cases gets sent
to the default window function, which, as you know, is called DefWindowProc.
DefWindowProc is analogous to the HandleEvent function shown earlier.
The important point to understand is that Windows messages contain the information
that drives the entire operating environment. Almost everything that happens inside
Windows is a message; if you really want to tap into the power of BCB, you need to
understand how these messages work.
One of the tricky parts of Windows event-oriented programming is extracting information
from the WPARAM and LPARAM variables passed to the window function.
In most cases, BCB frees you from the necessity of performing this task. For instance,
if you create an event for the OnMouseDown property, BCB directly tells
you the X value and Y value where the event occurred. As a programmer,
you don't have to struggle to get the event and its associated values. As you will
see in the next section, everything about the event is shown to you in a simple and
straightforward manner.
NOTE: WPARAM and LPARAM
are the types of the parameters passed to a standard WndProc as is declared
in WinUser.h:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
These variables are usually given names such as wParam and lParam
or WParam and LParam:
WndProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam);
You will find that I sometimes refer to these parameters by type in all caps,
and sometimes as variable identifiers with mixed caps and small letters. In all cases,
I am referring to the same variables passed to the user from the system. Even in
the discussion of the TMessage type, which occurs later in the chapter,
I am still referring to these same parameters, only in a slightly different context.
Specifically, TMessage is a VCL type that contains exact copies of these
parameters. The X and Y parameters passed to an OnMouseDown
event are not exact copies of WPARAM and LPARAM, but variables
designed to display information originally contained in WPARAM or LPARAM
after they have been parsed by the VCL.
Using Sets to Track Messages
Rather than ask you to parse the LPARAM and WPARAM parameters,
BCB performs this chore for you and then passes the information on in the form of
parameters:
void __fastcall TForm1::FormMouseDown(
TObject *Sender,
TMouseButton Button,
TShiftState Shift,
int X,
int Y)
This is by far the most convenient way for you to handle events. BCB can also
give direct access to the values sent to you by the operating system. That is, you
can handle WPARAM and LPARAM directly if you want. After you have
studied the EVENTS2 program and learned more about sets, I'll show you exactly how
to get at that raw data.
Take a moment to consider the Shift parameter shown in the FormMouseDown
header. Shift is declared to be of type TShiftState:
enum Classes_1 { ssShift, ssAlt, ssCtrl, ssLeft, ssRight, ssMiddle, ssDouble };
typedef Set<Classes_1, ssShift, ssDouble> TShiftState;
TShiftState is a set, that is, it's an instance of the Set template
class from SYSDEFS.H. To find out whether a particular element is a member
of the set passed to you by BCB, you can perform simple tests using the Contains
method:
ShiftKey->Checked = Shift.Contains(ssShift);
ControlKey->Checked = Shift.Contains(ssCtrl);
LeftButton->Checked = Shift.Contains(ssLeft);
RightButton->Checked = Shift.Contains(ssRight);
This code asks whether the element ssRight is in the set passed to you
via the Shift variable. If it is, the code sets the state of a TCheckBox
component.
Here is how you can declare a set at runtime:
TShiftState LeftShift;
LeftShift << ssLeft << ssShift;
Given this set, the Contains operator returns True if you ask
about ssLeft or ssShift.
Besides the important Contains method, there are three key operators
you can use with the Set class:
+ Union
- Difference
* Intersection
All three of these operators return a set, whereas Contains returns a
Boolean value. The SETSEXP program, shown in Listing 4.2, shows how to work with
these key elements of the Set template class.
Listing 4.2. The SETEXP program
shows how to use operators to track the members of sets such as TShiftState.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void TForm1::CheckState(TShiftState Shift)
{
ShiftKey->Checked = Shift.Contains(ssShift);
ControlKey->Checked = Shift.Contains(ssCtrl);
LeftButton->Checked = Shift.Contains(ssLeft);
RightButton->Checked = Shift.Contains(ssRight);
}
void __fastcall TForm1::UnionClick(TObject *Sender)
{
AnsiString Operators[3] = {"+", "*", "-"};
TShiftState FinalSet;
TShiftState LeftShift;
TShiftState LeftCtrl;
LeftShift << ssLeft << ssShift;
LeftCtrl << ssLeft << ssCtrl;
switch (TOptType(dynamic_cast <TBitBtn*>(Sender)->Tag))
{
case otUnion:
FinalSet = LeftShift + LeftCtrl;
break;
case otIntersection:
FinalSet = LeftShift * LeftCtrl;
break;
case otDifference:
FinalSet = LeftShift - LeftCtrl;
break;
}
CheckState(FinalSet);
Label2->Caption = Operators[dynamic_cast<TBitBtn *>(Sender)->Tag];
}
void __fastcall TForm1::Label4MouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
CheckState(Shift);
}
The SETEXP program shows how you can read and manipulate sets. In particular,
you work with sets of type TShiftState. When you are finished studying the
program, you will have all the knowledge you need to work with BCB sets.
The main form for the SETEXP program consists of four checkboxes, two panels,
four labels, and three bitbtns, as shown in Figure 4.3. The four checkboxes are placed
on top of the first panel, and the fourth label is placed on the top of the second
panel.
FIGURE
4.3. The SETEXP program's main form enables
you to manipulate variables of type TShiftState.
SETEXP tells you whether the Shift or Ctrl keys are pressed when the mouse is
clicked, and it tells you whether the user pressed the right or left mouse button.
The code also shows how to use the Intersection, Union, and Difference
operators.
The key method in the SETEXP program looks at a variable of type TShiftState
and displays its contents to the user through the program's radio buttons:
void TForm1::CheckState(TShiftState Shift)
{
ShiftKey->Checked = Shift.Contains(ssShift);
ControlKey->Checked = Shift.Contains(ssCtrl);
LeftButton->Checked = Shift.Contains(ssLeft);
RightButton->Checked = Shift.Contains(ssRight);
}
This code takes advantage of the fact that Contains returns a Boolean
variable, and the Checked property of a radio button is also declared to
be of type Boolean. As a result, you can test to see whether a particular element
is part of the Shift set. If it is, you can easily set the Checked
state of a radio button to record the result. For example, in the preceding code,
if ssShift is part of the current set, the Shift Key radio button is checked.
Two different routines pass variables of type TShiftState to the CheckState
method. The first routine is called whenever the user clicks in the panel at the
bottom of the program or the label that rests on the panel:
void __fastcall TForm1::Label4MouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
CheckState(Shift);
}
This code passes the Shift variable on to CheckState, which
displays the contents of the variable to the user. For instance, if the Shift key
is being held down and the right mouse button is pressed, Label4MouseDown
is called. Label4MouseDown then passes the Shift variable to CheckState,
and CheckState causes the ShiftKey and RightButton controls
to be checked. The other two radio buttons are left unchecked.
There are three bitbtns on the right side of the main form. They are labeled Union,
Intersection, and Difference. Clicking any of these buttons demonstrates one of the
non-Boolean set operators. All three buttons have their OnClick event set
to the following function:
void __fastcall TForm1::UnionClick(TObject *Sender)
{
AnsiString Operators[3] = {"+", "*", "-"};
TShiftState FinalSet;
TShiftState LeftShift;
TShiftState LeftCtrl;
LeftShift << ssLeft << ssShift;
LeftCtrl << ssLeft << ssCtrl;
switch (TOptType(dynamic_cast <TBitBtn*>(Sender)->Tag))
{
case otUnion:
FinalSet = LeftShift + LeftCtrl;
break;
case otIntersection:
FinalSet = LeftShift * LeftCtrl;
break;
case otDifference:
FinalSet = LeftShift - LeftCtrl;
break;
}
CheckState(FinalSet);
Label2->Caption = Operators[dynamic_cast<TBitBtn *>(Sender)->Tag];
}
The UnionClick method declares three variables of type TShiftState.
Two of these variables are used to create sets that are used by the rest of the SetBtnClick
method:
LeftShift << ssLeft << ssShift;
LeftCtrl << ssLeft << ssCtrl;
The first line assigns the LeftShift variable to a set that contains
the values ssLeft and ssShift. The next line assigns the LeftCtrl
variable to a set that contains ssLeft and ssCtrl. The rest of
this method enables the user to see the union, intersection, and difference of these
two sets.
The switch statement in the middle of the SetBtnClick method
detects which of the three bitbtns the user clicked. This is the old, time-honored
technique featuring the use of an enumerated type and the assignment of zero-based
ordinal values to the Tag field of each button.
If the user clicks the Union button, the FinalSet variable is set to
the union of the LeftShift and LeftCtrl variables:
FinalSet = LeftShift + LeftCtrl;
A click on the Intersection button executes the following code:
FinalSet = LeftShift * LeftCtrl;
The difference of the sets is calculated if the user clicks the Difference button:
FinalSet = LeftShift - LeftCtrl;
After the switch statement ensures the selection of the proper operator,
the FinalSet value is passed to CheckState and its contents are
displayed to the user. For instance, if the user clicks the Union button, the LeftButton,
ShiftKey, and ControlKey radio buttons are all checked. The Intersection button causes
the LeftKey to be checked, and the Difference button causes the ShiftKey to be checked.
Here is another way of looking at the work accomplished by these operators:
[ssLeft, ssShift] + [ssLeft, ssCtrl] = [ssLeft, ssShift, ssCtrl];
[ssLeft, ssShift] * [ssLeft, ssCtrl] = [ssLeft]
[ssLeft, ssShift] - [ssLeft, ssCtrl] = [ssShift]
To help the user understand exactly what is happening, the current set operation
is displayed at the top of the form. For instance, if the user clicks the Union button,
the following expression is shown to the user:
LeftShift + LeftCtrl
Throughout a run of the program, the words LeftShift and LeftCtrl
are displayed to the user in a pair of TLabels. A third label displays +,
-, or *, depending on the current state of the program:
Label2->Caption = Operators[dynamic_cast<TBitBtn *>(Sender)->Tag];
In this code, Operators is an array of three strings that contains the
operators that return a set:
AnsiString Operators[3] = {"+", "*", "-"};
The SETEXP program gives you enough information that you should be able to work
with the sets that are passed to BCB event handlers. The code shown here defines
the way sets are usually handled in all BCB programs. However, you can actually directly
manipulate the raw data that represents a BCB set. Techniques for performing these
manipulations are shown in the GetShift method, which is part of the program
examined in the next section of this chapter. You can also review the information
on sets in Chapter 3.
Tracking the Mouse and Keyboard
You now know enough to begin an in-depth study of the main event handlers used
by BCB forms and controls. The EVENTS2 program, shown in Listings 4.3 through 4.5,
enables you to trace the occurrence of all the keyboard or mouse interrupts generated
during the run of a program.
Listing 4.3. The EVENTS2 header
and main form provide a detailed look at how to track events.
//--------------------------------------------------------------------------
#ifndef MainH
#define MainH
//--------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>
//--------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:
TPanel *Panel1;
TLabel *Label1;
TLabel *LMouseMove;
TLabel *Label3;
TLabel *LMouseDown;
TLabel *Label5;
TLabel *LKeyDown;
TLabel *Label7;
TLabel *LKeyUp;
TLabel *Label9;
TLabel *LMouseUp;
TLabel *Label11;
TLabel *LWidth;
TLabel *Label13;
TLabel *LHeight;
TLabel *LSpecialMouse;
TLabel *Label16;
void __fastcall FormResize(TObject *Sender);
void __fastcall FormPaint(TObject *Sender);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift);
void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y);
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
private:
MESSAGE void MyMouseMove(TWMMouse &Message);
public:
virtual __fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_MOUSEMOVE, TWMMouse, MyMouseMove);
END_MESSAGE_MAP(TForm);
};
//--------------------------------------------------------------------------
extern TForm1 *Form1;
//--------------------------------------------------------------------------
#endif
Listing 4.4. The EVENTS2 main
form provides a detailed look at how to track events.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#include "VKeys1.h"
#include "Binary.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void TForm1::MyMouseMove(TWMMouse &Message)
{
TForm::Dispatch(&Message);
LSpecialMouse->Caption = "X " + IntToStr(Message.XPos) +
" Y " + IntToStr(Message.YPos);
}
void __fastcall TForm1::FormResize(TObject *Sender)
{
LHeight->Caption = IntToStr(Width);
LWidth->Caption = IntToStr(Height);
}
void __fastcall TForm1::FormPaint(TObject *Sender)
{
Canvas->Font->Name = "New Times Roman";
Canvas->Font->Size = 48;
Canvas->TextOut(1, Panel1->Height, "Mouse Zone");
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
LMouseUp->Caption = "X " + IntToStr(X) + " Y " +
IntToStr(Y) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift)
{
LKeyUp->Caption = GetKey(Key) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
LKeyDown->Caption = GetKey(Key) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y)
{
LMouseMove->Caption = "X " + IntToStr(X) + " Y " +
IntToStr(Y) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
LMouseDown->Caption = GetShift(Shift);
}
Listing 4.5. The VKeys unit is
used by the EVENTS2 program.
///////////////////////////////////////
// Vkeys.cpp
// Project Name: Events2
// Copyright (c) 1997 by Charles Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "VKeys1.h"
AnsiString MessageArray[5] =
{"WM_CHAR", "WM_KEY", "WM_MOUSEMOVE", "WM_MOUSEDOWN", "WM_MOUSEUP"};
AnsiString ButtonArray[3] =
{"mbLeft", "mbRight", "mbCenter"};
AnsiString ShiftArray[8] = {"ssShift", "ssAlt", "ssCtrl", "ssLeft",
"ssRight", "ssMiddle", "ssDouble", "ssUnknown"};
AnsiString GetShift(TShiftState State)
{
int B = 0, i;
AnsiString S;
for (i = 0; i <= 7; i++)
{
if (State.Contains(i))
S = S + " " + ShiftArray[i];
}
return S;
}
AnsiString GetKey(WORD K)
{
AnsiString S;
switch (K)
{
case VK_LBUTTON: S = "VK_LButton"; break;
case VK_RBUTTON : S = "VK_RBUTTON"; break;
case VK_CANCEL : S = "VK_CANCEL"; break;
case VK_MBUTTON : S = "VK_MBUTTON"; break;
case VK_BACK : S = "VK_BACK"; break;
case VK_TAB : S = "VK_TAB"; break;
case VK_CLEAR : S = "VK_CLEAR"; break;
case VK_RETURN : S = "VK_RETURN"; break;
case VK_SHIFT : S = "VK_SHIFT"; break;
case VK_CONTROL : S = "VK_CONTROL"; break;
case VK_MENU : S = "VK_MENU"; break;
case VK_PAUSE : S = "VK_PAUSE"; break;
case VK_CAPITAL : S = "VK_CAPITAL"; break;
case VK_ESCAPE : S = "VK_ESCAPE"; break;
case VK_SPACE : S = "VK_SPACE"; break;
case VK_PRIOR : S = "VK_PRIOR"; break;
case VK_NEXT : S = "VK_NEXT"; break;
case VK_END : S = "VK_END"; break;
case VK_HOME : S = "VK_HOME"; break;
case VK_LEFT : S = "VK_LEFT"; break;
case VK_UP : S = "VK_UP"; break;
case VK_RIGHT : S = "VK_RIGHT"; break;
case VK_DOWN : S = "VK_DOWN"; break;
case VK_SELECT : S = "VK_SELECT"; break;
case VK_PRINT : S = "VK_PRINT"; break;
case VK_EXECUTE : S = "VK_EXECUTE"; break;
case VK_SNAPSHOT : S = "VK_SNAPSHOT"; break;
case VK_INSERT : S = "VK_INSERT"; break;
case VK_DELETE : S = "VK_DELETE"; break;
case VK_HELP : S = "VK_HELP"; break;
case VK_NUMPAD0 : S = "VK_NUMPAD0"; break;
case VK_NUMPAD1 : S = "VK_NUMPAD1"; break;
case VK_NUMPAD2 : S = "VK_NUMPAD2"; break;
case VK_NUMPAD3 : S = "VK_NUMPAD3"; break;
case VK_NUMPAD4 : S = "VK_NUMPAD4"; break;
case VK_NUMPAD5 : S = "VK_NUMPAD5"; break;
case VK_NUMPAD6 : S = "VK_NUMPAD6"; break;
case VK_NUMPAD7 : S = "VK_NUMPAD7"; break;
case VK_NUMPAD8 : S = "VK_NUMPAD8"; break;
case VK_NUMPAD9 : S = "VK_NUMPAD9"; break;
case VK_MULTIPLY : S = "VK_MULTIPLY"; break;
case VK_ADD : S = "VK_ADD"; break;
case VK_SEPARATOR : S = "VK_SEPARATOR"; break;
case VK_SUBTRACT : S = "VK_SUBTRACT"; break;
case VK_DECIMAL : S = "VK_DECIMAL"; break;
case VK_DIVIDE : S = "VK_DIVIDE"; break;
case VK_F1 : S = "VK_F1"; break;
case VK_F2 : S = "VK_F2"; break;
case VK_F3 : S = "VK_F3"; break;
case VK_F4 : S = "VK_F4"; break;
case VK_F5 : S = "VK_F5"; break;
case VK_F6 : S = "VK_F6"; break;
case VK_F7 : S = "VK_F7"; break;
case VK_F8 : S = "VK_F8"; break;
case VK_F9 : S = "VK_F9"; break;
case VK_F10 : S = "VK_F10"; break;
case VK_F11 : S = "VK_F11"; break;
case VK_F12 : S = "VK_F12"; break;
case VK_F13 : S = "VK_F13"; break;
case VK_F14 : S = "VK_F14"; break;
case VK_F15 : S = "VK_F15"; break;
case VK_F16 : S = "VK_F16"; break;
case VK_F17 : S = "VK_F17"; break;
case VK_F18 : S = "VK_F18"; break;
case VK_F19 : S = "VK_F19"; break;
case VK_F20 : S = "VK_F20"; break;
case VK_F21 : S = "VK_F21"; break;
case VK_F22 : S = "VK_F22"; break;
case VK_F23 : S = "VK_F23"; break;
case VK_F24 : S = "VK_F24"; break;
case VK_NUMLOCK : S = "VK_NUMLOCK"; break;
case VK_SCROLL : S = "VK_SCROLL"; break;
default:
S = K;
}
return S;
}
EVENTS2 shows how to extract the full content of a message sent to you by BCB.
The main form for the program (shown in Figure 4.4) provides information on a wide
range of mouse and keyboard-generated events.
FIGURE
4.4. The EVENTS2 program tracks key Windows
events as they occur.
To use the program, simply compile and run it. Click the mouse in random locations
and strike any of the keys on the keyboard. Just rattle away; you won't do any harm
unless you press Ctrl+Alt+Del. Every time you move the mouse, click the mouse or
strike a key; the exact nature of the event that occurred is shown in the main form
of the program. For instance, if you move the mouse, its current location is shown
on the form. If you press the F1 key while the Ctrl key is pressed, those keys' values
are displayed on the form.
If you don't have the MyMouseMove function defined, the FormMouseMove
event handler in the EVENTS2 window tracks the current location of the mouse
and the state of its buttons. It does this by responding to OnMouseMove
events:
void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y)
{
LMouseMove->Caption = "X " + IntToStr(X) + " Y " +
IntToStr(Y) + " " + GetShift(Shift);
}
The method for tracking the X and Y values is fairly intuitive.
X stands for the current column, and Y stands for the current row,
with columns and rows measured in pixels. Before these values can be shown to the
user, they need to be translated into strings by the IntToStr function.
Nothing could be simpler than the techniques used to record the current location
of the mouse.
The technique for recording the current shift state, however, is a bit more complex.
As you saw earlier, the elements of this set track all the possible states of the
Shift, Alt, and Ctrl keys, as well as the mouse buttons.
The Set class makes it a simple matter to create a function that will
find all the currently selected elements of a variable of type TShiftState:
AnsiString GetShift(TShiftState State)
{
int B = 0, i;
AnsiString S;
for (i = 0; i <= 7; i++)
{
if (State.Contains(i))
S = S + " " + ShiftArray[i];
}
return S;
}
The preceding code takes advantage of the following constant array:
AnsiString ShiftArray[8] = {"ssShift", "ssAlt", "ssCtrl", "ssLeft",
"ssRight", "ssMiddle", "ssDouble", "ssUnknown"};
More specifically, the code checks to see whether the first possible element of
the set is active, and if it is, "ssShift" is added to the string
returned by the function. If the next element in the enumerated type underlying the
set is present, the string "ssAlt" is added to the string returned
by the function, and so on. As you move through the elements in the underlying enumerated
type, you get a picture of the current state of the mouse and keyboard. For instance,
if the Shift and Ctrl keys are pressed, as well as the right mouse button, the string
returned by the function looks like this:
ssShift ssCtrl ssRight
Trapping Virtual Keys
When keys are pressed in a Windows program, two different messages can be sent
to your program. One message is called WM_KEYDOWN, and it is sent whenever
any key on the keyboard is pressed. The second message is called WM_CHAR,
and it is sent when one of the alphanumeric keys is pressed. In other words, if you
press the A key, you get both a WM_KEYDOW and a WM_CHAR
message. If you press the F1 key, only the WM_KEYDOWN message is sent.
OnKeyPress event handlers correspond to WM_CHAR messages, and
OnKeyDown events correspond to WM_KEYDOWN events. That's why OnKeyPress
handlers are passed a Key variable that is of type char, and OnKeyDown
handlers are passed a Key variable that is of type WORD.
When you get a WM_KEYDOWN message, you need to have some way of translating
that message into a meaningful value. To help with this chore, Windows declares a
set of virtual key constants that all start with vk. For example, if you
press the F1 key, the Key variable passed to an OnKeyDown event
is set to VK_F1, in which the letters vk stand for virtual key.
The virtual key codes are found in the WINDOWS unit and also in the online
help under Virtual Key Codes.
You can test to see which virtual key has been pressed by writing code that looks
like this:
if (Key == VK_CANCEL)
DoSomething();
This code simply tests to see whether a particular key has been pressed. If it
has, the code calls the DoSomething function.
To help you understand virtual keys, the GetKey method from the VKEYS
unit returns a string stating exactly what key has been pressed:
AnsiString GetKey(WORD K)
{
AnsiString S;
switch (K)
{
case VK_LBUTTON: S = "VK_LButton"; break;
case VK_RBUTTON : S = "VK_RBUTTON"; break;
case VK_CANCEL : S = "VK_CANCEL"; break;
case VK_MBUTTON : S = "VK_MBUTTON"; break;
case VK_BACK : S = "VK_BACK"; break;
case VK_TAB : S = "VK_TAB"; break;
case VK_CLEAR : S = "VK_CLEAR"; break;
case VK_RETURN : S = "VK_RETURN"; break;
case VK_SHIFT : S = "VK_SHIFT"; break;
case VK_CONTROL : S = "VK_CONTROL"; break;
case VK_MENU : S = "VK_MENU"; break;
case VK_PAUSE : S = "VK_PAUSE"; break;
case VK_CAPITAL : S = "VK_CAPITAL"; break;
case VK_ESCAPE : S = "VK_ESCAPE"; break;
case VK_SPACE : S = "VK_SPACE"; break;
case VK_PRIOR : S = "VK_PRIOR"; break;
case VK_NEXT : S = "VK_NEXT"; break;
case VK_END : S = "VK_END"; break;
case VK_HOME : S = "VK_HOME"; break;
case VK_LEFT : S = "VK_LEFT"; break;
case VK_UP : S = "VK_UP"; break;
case VK_RIGHT : S = "VK_RIGHT"; break;
case VK_DOWN : S = "VK_DOWN"; break;
case VK_SELECT : S = "VK_SELECT"; break;
case VK_PRINT : S = "VK_PRINT"; break;
case VK_EXECUTE : S = "VK_EXECUTE"; break;
case VK_SNAPSHOT : S = "VK_SNAPSHOT"; break;
case VK_INSERT : S = "VK_INSERT"; break;
case VK_DELETE : S = "VK_DELETE"; break;
case VK_HELP : S = "VK_HELP"; break;
case VK_NUMPAD0 : S = "VK_NUMPAD0"; break;
case VK_NUMPAD1 : S = "VK_NUMPAD1"; break;
case VK_NUMPAD2 : S = "VK_NUMPAD2"; break;
case VK_NUMPAD3 : S = "VK_NUMPAD3"; break;
case VK_NUMPAD4 : S = "VK_NUMPAD4"; break;
case VK_NUMPAD5 : S = "VK_NUMPAD5"; break;
case VK_NUMPAD6 : S = "VK_NUMPAD6"; break;
case VK_NUMPAD7 : S = "VK_NUMPAD7"; break;
case VK_NUMPAD8 : S = "VK_NUMPAD8"; break;
case VK_NUMPAD9 : S = "VK_NUMPAD9"; break;
case VK_MULTIPLY : S = "VK_MULTIPLY"; break;
case VK_ADD : S = "VK_vkADD"; break;
case VK_SEPARATOR : S = "VK_SEPARATOR"; break;
case VK_SUBTRACT : S = "VK_SUBTRACT"; break;
case VK_DECIMAL : S = "VK_DECIMAL"; break;
case VK_DIVIDE : S = "VK_DIVIDE"; break;
case VK_F1 : S = "VK_F1"; break;
case VK_F2 : S = "VK_F2"; break;
case VK_F3 : S = "VK_F3"; break;
case VK_F4 : S = "VK_F4"; break;
case VK_F5 : S = "VK_F5"; break;
case VK_F6 : S = "VK_F6"; break;
case VK_F7 : S = "VK_F7"; break;
case VK_F8 : S = "VK_F8"; break;
case VK_F9 : S = "VK_F9"; break;
case VK_F10 : S = "VK_F10"; break;
case VK_F11 : S = "VK_F11"; break;
case VK_F12 : S = "VK_F12"; break;
case VK_F13 : S = "VK_F13"; break;
case VK_F14 : S = "VK_F14"; break;
case VK_F15 : S = "VK_F15"; break;
case VK_F16 : S = "VK_F16"; break;
case VK_F17 : S = "VK_F17"; break;
case VK_F18 : S = "VK_F18"; break;
case VK_F19 : S = "VK_F19"; break;
case VK_F20 : S = "VK_F20"; break;
case VK_F21 : S = "VK_F21"; break;
case VK_F22 : S = "VK_F22"; break;
case VK_F23 : S = "VK_F23"; break;
case VK_F24 : S = "VK_F24"; break;
case VK_NUMLOCK : S = "VK_NUMLOCK"; break;
case VK_SCROLL : S = "VK_SCROLL"; break;
default:
S = char(K);
}
return S;
}
This function is really just a giant case statement that checks to see
whether the Key variable is equal to any of the virtual keys. If it is not,
the code assumes that it must be one of the standard keys between A and
Z. (See the else clause in the code to see how these standard keys
are handled.)
As explained in the last paragraph, the virtual key codes do not cover normal
letters such as A, B, and C. In other words, there is
no value VK_A or VK_B. To test for these letters, just use the
standard ASCII values. In other words, test whether Key is equal to 65,
or whether char(key) = `A'. The point here is that these letters already
have key codes. That is, the key codes for these letters are the literal values A,
B, C, and so on. Because these are perfectly serviceable values,
there is no need to create virtual key codes for the standard letters of the alphabet,
or for numbers.
To see the value as a number, make the default section of the code look like this:
S = K;
Then if you press the A key, you get back 65. If you want to see the
letter `A' instead, write the following:
S = char(K);
You probably won't have much use for the GetKey routine in a standard
BCB program. However, it is useful when you are trying to understand virtual keys
and the OnKeyDown event. As a result, I have included it in this program.
Handling Events Directly
If you look at the bottom of the EVENTS2 form, you see that there is
a special event that tracks the position of the mouse. The EVENTS2 program tracks
the mouse movements in two different ways because I wanted to show you that you can
get information about the mouse either by responding to OnMouseMove events
or by directly tracking WM_MOUSEMOVE messages.
Here is how you declare a function that is going to directly capture a message:
class TForm1 : public TForm
{
__published:
... // Declarations omitted
private:
MESSAGE void MyMouseMove(TWMMouse &Message);
public:
virtual __fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_MOUSEMOVE, TWMMouse, MyMouseMove);
END_MESSAGE_MAP(TForm);
};
The declaration shown here tells BCB that you want to respond directly when the
operating system informs your program that the mouse has moved. In other words, you
don't want the BCB VCL to trap the message first and then pass it on to you in an
OnMouseMove event. Instead, you just want the message sent straight to you
by the operating system, as if you were working with one of the Windows API programs
shown earlier in the book. In short, you're telling the VCL: "Yes, I know you
can make this task very simple and can automate nearly the entire process by using
visual tools. That's nice of you, but right now I want to get the real event itself.
I have some reason of my own for wanting to get very close to the metal. As a result,
I'm going to grab the message before you ever get a chance to look at it!"
Here's the code for the MyMouseMove function:
void TForm1::MyMouseMove(TWMMouse &Message)
{
TForm::Dispatch(&Message);
LSpecialMouse->Caption = "X " + IntToStr(Message.XPos) +
" Y " + IntToStr(Message.YPos);
}
You can see that the code begins by calling the Dispatch method inherited
from TObject. If you didn't make this call, the program would still run,
but the OnMouseMove event would never be sent to the FormMouseMove
function. It isn't an error if you don't pass the message back to BCB. You can either
keep the message for yourself or pass it on, as you prefer.
If you omit the call to Dispatch from the MySpecialMouse function,
the FormMouseMove method in the EVENTS2 program is no longer called. In
other words, you are directly trapping WM_MOUSEMOVE messages and not passing
them on to the VCL. As a result, the VCL does not know that the event occurred, and
FormMouseMove is not called.
The explanation in the last paragraph might not be easy to grasp unless you actually
experiment with the EVENTS2 program. You should run the program once with the default
version of the MyMouseMove method, and once with the call to Inherited
commented out:
void TForm1::MyMouseMove(TWMMouse &Message)
{
// TForm::Dispatch(&Message);
LSpecialMouse->Caption = "X " + IntToStr(Message.XPos) +
" Y " + IntToStr(Message.YPos);
}
Notice that when you run the program this way, the OnMouseMove message
at the top of the form is left blank.
If you look at the header for the MyMouseMove function, you can see that
it is passed a parameter of type TWMMouse. As you recall, the TWMMouse
record, found in MESSAGES.PAS, looks like this:
struct TWMMouse
{
unsigned int Msg;
long Keys;
union
{
struct
{
Windows::TSmallPoint Pos;
long Result;
};
struct
{
short XPos;
short YPos;
};
};
};
If you break out both of the options shown in this variant record, you can further
simplify this record by writing
struct TWMMouse
{
unsigned int Msg;
long Keys;
Windows::TSmallPoint Pos;
long Result;
};
or
struct TWMMouse
{
unsigned int Msg;
long Keys;
short XPos;
short YPos;
};
For most users, one of these two views will be the most useful way to picture
the record.
The same information is present in a TWMMouse record that you would find
if you responded to an OnMouseMove or OnMouseDown event. If appropriate,
you can find out the row and column where the mouse is located, what key is pressed,
and what state the Shift, Alt, and Ctrl keys are in. To pursue this matter further,
you should look up WM_MOUSEMOVE and WM_MOUSEDOWN messages in the
online help.
TWMMouse plays the same role in a BCB program that message crackers from
WINDOWSX.H play in a C++ program. In other words, they automatically break
out the values passed in lParam or wParam parameters of the WndProc.
However, if you want, you can pass a variable of type TMessage as the parameter
sent to the WM_MOUSEMOVE message handler.
Because TMessage and TWMMouse are both the same size, BCB doesn't
care which one you use when trapping WM_MOUSEMOVE events. It's up to you
to decide how you want to crack the wParam and lParam parameters
passed to the WndProc.
In this section, you have learned something about directly handling Windows messages.
When you write code that captures messages directly, you are in a sense reverting
back to the more complicated model of programming found in Borland C++ 5.x. However,
there are times when it is helpful to get close to the machine; BCB lets you get
there if that is what you need to do.
Menu IDs, Handling WM_COMMAND,
Finding TForms WndProc
In standard Windows programming, as it was conducted before the appearance of
visual tools, one of the most important messages was WM_COMMAND. This message
was sent to a program every time the user selected a menu item or a button, or clicked
almost any other control that is part of the current program. Furthermore, each of
the buttons, menu items, and other controls in a program had a special ID, which
was assigned by the programmer. This ID was passed to WM_COMMAND handlers
in the wParam variable.
BCB handles WM_COMMAND messages in such a way that you almost never have
to think about them. For instance, you can get clicks on a button or menu by using
the delegation model. Standard BCB controls still have IDs, but BCB assigns these
numbers automatically, and there is no obvious way for you to learn the value of
these IDs.
Despite BCB's capability to simplify this aspect of Windows programming, there
are still times when you want to get down to the bare bones and start handling WM_COMMAND
messages yourself. In particular, you will want to find a way to discover the ID
associated with a particular command, and you will want to trap that ID inside a
WM_COMMAND handler.
WARNING: If you are new to good RAD programming
tools like BCB, you are likely to think you need to get hold of menu IDs a lot more
often than you really need to get hold of them. When I first came to this paradigm,
I was used to using menu IDs and I had a lot of tricks that were associated with
them. As a result, I was determined to use them in my first RAD programs. That was
a mistake, and I did nothing but waste time trying to walk down that road.
If the developers of the VCL thought you had any need at all for these ids, they
would have made them readily available. The fact that they are a bit difficult to
get at should tell you that it is very rare for anyone to ever need to access these
items. It wouldn't have been hard to make these IDs available; there was simply no
need to do it, and some good reasons to hint broadly that RAD had some better ways
to do this type of programming. The obscurity of menu IDs is not a weakness of RAD;
they are just a broad hint that you don't need to worry about that side of Windows
programming any more.
The people who designed the VCL are very bright folks who know the Windows API in
intimate, excruciating detail. Their goal was to create an easy-to-use programming
environment that still gives users full access to all the features of Windows. They
would never deliberately cut you off from something you needed.
Don't forget that both Delphi and BCB are built in the VCL by the same people who
created the VCL in the first place. One of the reasons the VCL is so good is that
it was honed by its creators during the process of creating the Delphi and BCB IDEs.
If there were something wrong with the way the VCL was designed, it became apparent
to the developers while they were creating the IDE, and it was fixed.
This was not a tool created cynically in a back room by a bunch of guys who looked
down on their own users as second-rate programmers. They themselves were the programmers
who first used their tools, so they did everything they could to build them correctly.
The MENUDEF program gives a general overview of how to handle WM_COMMAND
messages. The program enables you to discover the ID used by a series of menu items
and then enables you to trap these IDs when they are sent to a WM_COMMAND
handler in the form of a TMessage.wParam variable.
As a bonus, the program also shows how to use the TForm WndProc
method, which lets you set up your own window function that receives all the messages
sent to your form. It is extremely rare that you would ever need to override this
virtual method in your own programs, but I show you how to do it just so you will
understand a little more about how the VCL operates. You should be warned that this
is a dangerous method to override, because it might come into existence before your
controls are initialized and ready for use and might disappear after the controls
have been disposed. For instance, I check for WM_CLOSE messages, and once
I get one, I don't try to show you any more messages coming into the WndProc.
Needless to say, you would crash your program immediately if you overrode this method
and did not call its ancestor.
Figure 4.5 shows the form for the MENUDEF program. Here are the menu items that
you can't see in Figure 4.5:
Caption = `File'
Caption = `Open'
Caption = `Close'
Caption = `Exit'
Caption = `Edit'
Caption = `Cut'
Caption = `Copy'
Caption = `Paste'
FIGURE
4.5. The MENUDEF program uses a TMemo,
a TButton, and a TMainMenu control.
The code for the MENUDEF program is in Listings 4.6 and 4.7. You can see that it
features two standard BCB event handlers, as well as a WM_COMMAND handler
and the overridden WndProc virtual method.
Listing 4.6. The MENUDEF program
shows how to retrieve the ID of a BCB menu item.
#ifndef MainH
#define MainH
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Menus.hpp>
class TForm1 : public TForm
{
__published:
TMemo *Memo1;
TButton *MenuID;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *Open1;
TMenuItem *CLose1;
TMenuItem *Exit1;
TMenuItem *Exit2;
TMenuItem *Cut1;
TMenuItem *Copy1;
TMenuItem *Paste1;
TListBox *ListBox1;
TButton *MiniWinSight;
void __fastcall MenuIDClick(
TObject *Sender);
void __fastcall MiniWinSightClick(
TObject *Sender);
private:
MESSAGE void WMCommand(TMessage &Message);
int TotalMenuItems;
int MenuItemArray[100];
protected:
virtual void __fastcall WndProc(Messages::TMessage &Message);
public:
virtual __fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_COMMAND, TMessage, WMCommand);
END_MESSAGE_MAP(TForm);
};
extern TForm1 *Form1;
#endif
Listing 4.7. The MENUDEF program
uses a TMemo, a TButton, and a TMainMenu control.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
static bool ShowMessages = FALSE;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
int i;
TotalMenuItems = 0;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TMenuItem *>(Components[i]))
{
MenuItemArray[TotalMenuItems] =
dynamic_cast<TMenuItem*>(Components[i])->Command;
TotalMenuItems++;
}
}
void TForm1::WMCommand(TMessage &Message)
{
int i,j;
AnsiString S1(IntToStr(Message.WParam)), S3;
for (i = 0; i < TotalMenuItems; i++)
if (Message.WParam == MenuItemArray[i])
{
for (j = 0; j < ComponentCount; j++)
{
if (dynamic_cast<TMenuItem*>(Components[j]))
if (dynamic_cast<TMenuItem*>(Components[j])->Command ==
MenuItemArray[i])
S3 = dynamic_cast<TMenuItem*>(Components[j])->Caption;
}
S1 = "ID: " + S1 + "\rName: " + S3;
MessageBox(Handle, S1.c_str(), "Menu Item Info", MB_OK);
}
TForm::Dispatch(&Message);
}
void __fastcall TForm1::MenuIDClick(TObject *Sender)
{
int Command, i;
AnsiString Name;
Memo1->Lines->Clear();
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TMenuItem*>(Components[i]))
{
Command = dynamic_cast<TMenuItem*>(Components[i])->Command;
Name = dynamic_cast<TMenuItem*>(Components[i])->Caption;
Memo1->Lines->Add(Name + " = " + IntToStr(Command));
}
}
}
void __fastcall TForm1::MiniWinSightClick(
TObject *Sender)
{
ShowMessages = True;
}
void ShowMsg(AnsiString &S)
{
if (Form1->ListBox1)
Form1->ListBox1->Items->Add(S);
if (Form1->ListBox1->Items->Count > 6)
Form1->ListBox1->TopIndex = Form1->ListBox1->Items->Count - 5;
}
void HandleMessages(TMessage &Msg)
{
AnsiString S;
switch(Msg.Msg)
{
case WM_PAINT:
S = "wm_Paint";
ShowMsg(S);
break;
case WM_MOUSEMOVE:
S = IntToStr(Msg.LParamLo) + " " + IntToStr(Msg.LParamHi);
S = "WM_MOUSEMOVE " + S;
ShowMsg(S);
break;
case WM_LBUTTONDOWN:
S = IntToStr(Msg.LParamLo) + " " + IntToStr(Msg.LParamHi);
S = "WM_LBUTTONDOWN" + S;
ShowMsg(S);
break;
case WM_RBUTTONDOWN:
S = IntToStr(Msg.LParamLo) + " " + IntToStr(Msg.LParamHi);
S = "WM_RBUTTONDOWN" + S;
ShowMsg(S);
break;
/* case WM_NCHITTEST: // Uncomment WM_NCHITEST to see a flurry of messages.
S = "WM_NCHITTEST";
ShowMsg(S);
break; */
}
}
void __fastcall TForm1::WndProc(Messages::TMessage &Message)
{
if (Message.Msg == WM_CLOSE)
ShowMessages = FALSE;
if (ShowMessages)
HandleMessages(Message);
TForm::WndProc(Message);
}
The MENUDEF program has two features:
- If you click the button at the bottom of the main form, the program's memo
control displays a list of all the items in the menu, along with their IDs.
- If you click any of the menu items, a message box appears stating the name of
the menu item and its ID.
Here is the code that grabs the ID of all the menu items and displays the ID along
with the menu item's caption in a TMemo:
void __fastcall TForm1::MenuIDClick(TObject *Sender)
{
int Command, i;
AnsiString Name;
Memo1->Lines->Clear();
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TMenuItem*>(Components[i]))
{
Command = dynamic_cast<TMenuItem*>(Components[i])->Command;
Name = dynamic_cast<TMenuItem*>(Components[i])->Caption;
Memo1->Lines->Add(Name + " = " + IntToStr(Command));
}
}
}
The code begins by clearing the current contents of the memo control.
It then iterates through all the components on the form and finds any of them that
are of type TMenuItem. The next step is to get the ID and the caption of
the menu items. To get the ID, you need only reference the Command property
of the TMenuItem component. The caption can be retrieved the same way, and
then you can add this information to the list box.
The use of a dynamic_cast in the previous code demonstrates BCB's capability
to work with Run Time Type Information (RTTI). RTTI enables you to test the type
of a particular variable and respond accordingly. For instance, in this case the
program simply asks whether a particular component is of type TMenuItem;
if this Boolean question returns True, the program examines the component
in more depth.
The remaining code in the program captures the IDs of the menu items in an array
and then responds to WM_COMMAND messages generated by clicks in one of the
program's menu items. As explained earlier, the code displays a message box stating
that the menus are not yet functional. In the caption of the menu box, you can read
the ID of the control that was clicked.
The code that captures the menu items in an array occurs in a Forms constructor:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
int i;
TotalMenuItems = 0;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TMenuItem *>(Components[i]))
{
MenuItemArray[TotalMenuItems] =
dynamic_cast<TMenuItem*>(Components[i])->Command;
TotalMenuItems++;
}
}
This code is almost identical to the MenuIDClick method, except that
the IDs of the TMenuItems are stored in an array rather than being shown
in a TMemo. The declaration for the array looks like this:
int MenuItemArray[100];
The declaration for the WM_COMMAND handler should be familiar to you
by this time:
MESSAGE void WMCommand(TMessage &Message);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_COMMAND, TMessage, WMCommand);
END_MESSAGE_MAP(TForm);
Here, the message directive tells the compiler that this is a dynamic method and
the offset in the dynamic method table is established by the WM_COMMAND
constant.
The WMCommand method compares the values sent in Message.wParam
to the values in the MenuItemArray. If it finds a match, it displays the
message box previously described.
void TForm1::WMCommand(TMessage &Message)
{
int i,j;
AnsiString S1(IntToStr(Message.WParam)), S3;
for (i = 0; i < TotalMenuItems; i++)
if (Message.WParam == MenuItemArray[i])
{
for (j = 0; j < ComponentCount; j++)
{
if (dynamic_cast<TMenuItem*>(Components[j]))
if (dynamic_cast<TMenuItem*>(Components[j])->Command ==
MenuItemArray[i])
S3 = dynamic_cast<TMenuItem*>(Components[j])->Caption;
}
S1 = "ID: " + S1 + "\rName: " + S3;
MessageBox(Handle, S1.c_str(), "Menu Item Info", MB_OK);
}
TForm::Dispatch(&Message);
}
This code calls the Windows API MessageBox function rather than MessageDlg.
Here is the code that overrides the window function:
void __fastcall TForm1::WndProc(Messages::TMessage &Message)
{
if (Message.Msg == WM_CLOSE)
ShowMessages = FALSE;
if (ShowMessages)
HandleMessages(Message);
TForm::WndProc(Message);
}
The TMessage struct looks like this:
struct TMessage
{
unsigned int Msg;
union
{
struct
{
unsigned short WParamLo;
unsigned short WParamHi;
unsigned short LParamLo;
unsigned short LParamHi;
unsigned short ResultLo;
unsigned short ResultHi;
};
struct
{
long WParam;
long LParam;
long Result;
};
};
};
This structure has all the information in it that you would get if you were inside
a standard windows function. For all intents and purposes, you are inside a standard
window function. (See TApplication.Create in the Pascal source, as well
as the TForm object, for more information.)
At this stage I could begin a 400- or 500-page analysis of window functions, messages,
and the structure of the Windows operating system. (That was pretty much what I did
in Teach Yourself Windows 95 Programming in 21 Days (Sams Publishing), so there is
a source for this kind of information if you want it.) However, this book is not
about that kind of material, so I will leave this method hanging and let you explore
it to the degree to which the subject matter calls you. Remember, however, that this
method can be dangerous to handle, as it is active even when the controls on your
form have not been created, and it is still active after they have been destroyed.
One final point about working with the IDs of a control: If you build the interface
of your program and then don't change its menus or any other aspect of its interface,
the IDs that BCB associates with each control will (at least theoretically) remain
the same throughout the development of your application. This might give some people
the idea of using the MenuIDClick method, shown earlier, as a temporary
response to a click on one of your controls and the storing of the output to a file.
Thereafter, you would hope to know the ID associated with each of your controls and
could handle them in a WM_COMMAND routine. This technique is theoretically
possible, but I wouldn't recommend it as part of the framework for any serious application.
In short, if you have to know the ID of a particular control, I would determine it
in the FormCreate method, thereby ensuring that the ID is correct for that
build of your program.
Summary
In this chapter you learned how to handle events using the standard BCB delegation
model. You saw that when the need arises, you can circumvent this system and handle
events directly by employing message maps. You can also pass messages back to the
system by calling the inherited Dispatch method. Events represent one of
the most important subjects in BCB programming, and you probably shouldn't move on
until you have a good idea of what's going on in the EVENTS2 program.
This chapter also explained that you can handle wParam and lParam
variables directly. Furthermore, BCB gives you a way to parse the information passed
with events, so that you can place a custom record as the parameter to a message
handler. For instance, if Windows conceals a set of X and Y coordinates
inside the high or low words of wParam, BCB enables you to define a custom
record that automatically breaks up wParam into two separate variables called
X and Y. This is the same functionality that is found in the message
crackers provided in WINDOWSX.H. The TWMMouse record discussed
earlier is one of these records; TMessage is another. If you want, you can
create your own records that parse the information associated with standard BCB events
or with events that you create yourself. You are not limited to custom BCB handlers
such as TMessage or TWMMouse.
|